import nock from 'nock'
import type { CommandEndpoint, Protocol } from '@wdio/protocols'

import {
    WebDriverProtocol, MJsonWProtocol, AppiumProtocol,
    ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol
} from '@wdio/protocols'

const REGEXP_SESSION_ID = /\/[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}/
const SESSION_ID = 'XXX'
const protocols: Protocol[] = [
    WebDriverProtocol, MJsonWProtocol, AppiumProtocol,
    ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol
]

type RequestMethods = 'get' | 'post'
type protocolFlattenedType = { method: string, endpoint: string, commandData: CommandEndpoint }
const protocolFlattened: Map<string, protocolFlattenedType> = new Map()

export interface CommandMock {
    [commandName: string]: (...args: unknown[]) => nock.Interceptor
}

for (const protocol of protocols) {
    for (const [endpoint, methods] of Object.entries(protocol)) {
        for (const [method, commandData] of Object.entries(methods)) {
            protocolFlattened.set(commandData.command, { method, endpoint, commandData })
        }
    }
}

export default class WebDriverMock {
    command: CommandMock
    scope: nock.Scope
    constructor(host: string = 'localhost', port: number = 4444, public path: string = '/') {
        this.scope = nock(`http://${host}:${port}`, {
            encodedQueryParams: true
        })
        this.command = new Proxy({}, { get: this.get.bind(this) }) as unknown as CommandMock
    }

    /**
     * To allow random session IDs in url paths we have to set up a custom
     * matcher that strips out the sessionID part from the expected url
     * and actual url and replaces it with a constant session id
     * @param   {String}   expectedPath path to match against
     * @returns {Function}              to be called by Nock to match actual path
     */
    static pathMatcher(expectedPath: string): (path:string) =>boolean {
        return (path: string) => {
            const sessionId = path.match(REGEXP_SESSION_ID)

            /**
             * no session ID found so we can check against expected path directly
             */
            if (!sessionId) {
                return path === expectedPath
            }

            /**
             * remove the session ID from expected and actual path
             * to only compare non arbitrary parts
             */
            expectedPath = expectedPath.replace(':sessionId', SESSION_ID)
            path = path.replace(`${sessionId[0].slice(1)}`, SESSION_ID)
            return path === expectedPath
        }
    }

    get(_obj: unknown, commandName: string) {

        const { method, endpoint, commandData } = protocolFlattened.get(commandName) as protocolFlattenedType

        return (...args: unknown[]) => {
            let urlPath = endpoint
            for (const [i, param] of Object.entries(commandData.variables || [])) {
                urlPath = urlPath.replace(`:${param.name}`, args[parseInt(i)] as string)
            }

            if (method === 'POST') {
                const reqMethod = method.toLowerCase() as RequestMethods
                return this.scope[reqMethod](WebDriverMock.pathMatcher(urlPath), (body: Record<string, unknown>) => {
                    for (const param of commandData.parameters) {
                        /**
                         * check if parameter was set
                         */
                        if (!body[param.name]) {
                            return false
                        }

                        /**
                         * check if parameter has correct type
                         */
                        if (param.required && typeof body[param.name] === 'undefined') {
                            return false
                        }
                    }

                    /**
                     * all parameters are valid
                     */
                    return true
                })
            }

            const reqMethod = method.toLowerCase() as RequestMethods
            return this.scope[reqMethod](WebDriverMock.pathMatcher(urlPath))
        }
    }
}
